// Copyright (c) 2004-2008 MySQL AB, 2008-2009 Sun Microsystems, Inc.
//
// MySQL Connector/NET is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most 
// MySQL Connectors. There are special exceptions to the terms and 
// conditions of the GPLv2 as it is applied to this software, see the 
// FLOSS License Exception
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// This program is free software; you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published 
// by the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful, but 
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
// for more details.
//
// You should have received a copy of the GNU General Public License along 
// with this program; if not, write to the Free Software Foundation, Inc., 
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

using System;
using System.Collections;
using System.Text;
using System.Collections.Generic;
using MySql.Data.MySqlClient.Properties;
using System.Data;

namespace MySql.Data.MySqlClient
{
    /// <summary>
    /// Summary description for PreparedStatement.
    /// </summary>
    internal class PreparableStatement : Statement
    {
        private int executionCount;
        private int statementId;
        BitArray nullMap;
        List<MySqlParameter> parametersToSend = new List<MySqlParameter>();
        MySqlPacket packet;
        int dataPosition;
        int nullMapPosition;

        public PreparableStatement(MySqlCommand command, string text)
            : base(command, text)
        {
        }

        #region Properties

        public int ExecutionCount
        {
            get { return executionCount; }
            set { executionCount = value; }
        }

        public bool IsPrepared
        {
            get { return statementId > 0; }
        }

        public int StatementId
        {
            get { return statementId; }
        }

        #endregion

        public virtual void Prepare()
        {
            // strip out names from parameter markers
            string text;
            List<string> parameter_names = PrepareCommandText(out text);

            // ask our connection to send the prepare command
            MySqlField[] paramList = null;
            statementId = Driver.PrepareStatement(text, ref paramList);

            // now we need to assign our field names since we stripped them out
            // for the prepare
            for (int i = 0; i < parameter_names.Count; i++)
            {
                //paramList[i].ColumnName = (string) parameter_names[i];
                string parameterName = (string)parameter_names[i];
                MySqlParameter p = Parameters.GetParameterFlexible(parameterName, false);
                if (p == null)
                    throw new InvalidOperationException(
                        String.Format(Resources.ParameterNotFoundDuringPrepare, parameterName));
                p.Encoding = paramList[i].Encoding;
                parametersToSend.Add(p);
            }

            // now prepare our null map
            int numNullBytes = 0;
            if (paramList != null && paramList.Length > 0)
            {
                nullMap = new BitArray(paramList.Length);
                numNullBytes = (nullMap.Count + 7) / 8;
            }

            packet = new MySqlPacket(Driver.Encoding);
            
            // write out some values that do not change run to run
            packet.WriteByte(0);
            packet.WriteInteger(statementId, 4);
            packet.WriteByte((byte)0); // flags; always 0 for 4.1
            packet.WriteInteger(1, 4); // interation count; 1 for 4.1
            nullMapPosition = packet.Position;
            packet.Position += numNullBytes;  // leave room for our null map
            packet.WriteByte(1); // rebound flag
            // write out the parameter types
            foreach (MySqlParameter p in parametersToSend)
                packet.WriteInteger(p.GetPSType(), 2);
            dataPosition = packet.Position;
        }

        public override void Execute()
        {
            // if we are not prepared, then call down to our base
            if (!IsPrepared)
            {
                base.Execute();
                return;
            }

            //TODO: support long data here
            // create our null bitmap

            // we check this because Mono doesn't ignore the case where nullMapBytes
            // is zero length.
//            if (nullMapBytes.Length > 0)
  //          {
    //            byte[] bits = packet.Buffer;
      //          nullMap.CopyTo(bits, 
        //        nullMap.CopyTo(nullMapBytes, 0);

            // start constructing our packet
//            if (Parameters.Count > 0)
  //              nullMap.CopyTo(packet.Buffer, nullMapPosition);
            //if (parameters != null && parameters.Count > 0)
            //else
            //	packet.WriteByte( 0 );
            //TODO:  only send rebound if parms change

            // now write out all non-null values
            packet.Position = dataPosition;
            for (int i = 0; i < parametersToSend.Count; i++)
            {
                MySqlParameter p = parametersToSend[i];
                nullMap[i] = (p.Value == DBNull.Value || p.Value == null) ||
                    p.Direction == ParameterDirection.Output;
                if (nullMap[i]) continue;
                packet.Encoding = p.Encoding;
                p.Serialize(packet, true, Connection.Settings);
            }
            if (nullMap != null)
                nullMap.CopyTo(packet.Buffer, nullMapPosition);

            executionCount++;

            Driver.ExecuteStatement(packet);
        }

        public override bool ExecuteNext()
        {
            if (!IsPrepared)
                return base.ExecuteNext();
            return false;
        }

        /// <summary>
        /// Prepares CommandText for use with the Prepare method
        /// </summary>
        /// <returns>Command text stripped of all paramter names</returns>
        /// <remarks>
        /// Takes the output of TokenizeSql and creates a single string of SQL
        /// that only contains '?' markers for each parameter.  It also creates
        /// the parameterMap array list that includes all the paramter names in the
        /// order they appeared in the SQL
        /// </remarks>
        private List<string> PrepareCommandText(out string stripped_sql)
        {
            StringBuilder newSQL = new StringBuilder();
            List<string> parameterMap = new List<string>();

            int startPos = 0;
            string sql = ResolvedCommandText;
            MySqlTokenizer tokenizer = new MySqlTokenizer(sql);
            string parameter = tokenizer.NextParameter();
            while (parameter != null)
            {
                if (parameter.IndexOf(StoredProcedure.ParameterPrefix) == -1)
                {
                    newSQL.Append(sql.Substring(startPos, tokenizer.StartIndex - startPos));
                    newSQL.Append("?");
                    parameterMap.Add(parameter);
                    startPos = tokenizer.StopIndex;
                }
                parameter = tokenizer.NextParameter();
            }
            newSQL.Append(sql.Substring(startPos));
            stripped_sql = newSQL.ToString();
            return parameterMap;
        }

        public virtual void CloseStatement()
        {
            if (!IsPrepared) return;

            Driver.CloseStatement(statementId);
            statementId = 0;
        }
    }
}
